/*
 **** BEGIN LICENSE BLOCK *****
 * Version: EPL 2.0/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Eclipse Public
 * License Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.eclipse.org/legal/epl-v20.html
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * Copyright (C) 2013-2015 Charles O Nutter <headius@headius.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 2 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the EPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the EPL, the GPL or the LGPL.
 ***** END LICENSE BLOCK *****/

package org.jruby.java.codegen;

import static org.jruby.RubyInstanceConfig.JAVA_VERSION;
import static org.jruby.util.CodegenUtils.ci;
import static org.jruby.util.CodegenUtils.getBoxType;
import static org.jruby.util.CodegenUtils.p;
import static org.jruby.util.CodegenUtils.params;
import static org.jruby.util.CodegenUtils.prettyParams;
import static org.jruby.util.CodegenUtils.sig;
import static org.objectweb.asm.Opcodes.ACC_FINAL;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_PROTECTED;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Opcodes.ACC_SUPER;
import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;

import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyBasicObject;
import org.jruby.RubyClass;
import org.jruby.RubyClass.ConcreteJavaReifier;
import org.jruby.RubyModule;
import org.jruby.ast.executable.RuntimeCache;
import org.jruby.compiler.impl.SkinnyMethodAdapter;
import org.jruby.compiler.util.BasicObjectStubGenerator;
import org.jruby.exceptions.RaiseException;
import org.jruby.internal.runtime.methods.DynamicMethod;
import org.jruby.java.proxies.ConcreteJavaProxy;
import org.jruby.java.proxies.ConcreteJavaProxy.SplitCtorData;
import org.jruby.java.proxies.MapJavaProxy;
import org.jruby.javasupport.Java.JCreateMethod;
import org.jruby.javasupport.Java.JCtorCache;
import org.jruby.javasupport.JavaConstructor;
import org.jruby.javasupport.JavaUtil;
import org.jruby.runtime.Block;
import org.jruby.runtime.PositionAware;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ASM;
import org.jruby.util.ClassDefiningClassLoader;
import org.jruby.util.ClassDefiningJRubyClassLoader;
import org.jruby.util.Loader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;

/**
 * On fly .class generator (used for Ruby interface impls).
 *
 * @author headius
 */
public abstract class RealClassGenerator {

    private static final boolean DEBUG = false;

    private static final int V_BC = JAVA_VERSION; // version used for generated byte-code

    //public static Map<String, List<Method>> buildSimpleToAllMap(Class[] interfaces, String[] superTypeNames)
    //    throws SecurityException {
    //    return buildSimpleToAllMap(interfaces, superTypeNames, null);
    //}

    static Map<String, List<Method>> buildSimpleToAllMap(Class[] interfaces, String[] superTypeNames, RubyClass implClass)
        throws SecurityException {
        final LinkedHashMap<String, List<Method>> simpleToAll = new LinkedHashMap<>();
        // we're use the map's order to work-around bug when there's too getters for a property :
        // getFoo and isFoo in which case we make sure getFoo will come after isFoo in the map
        // so that the installed "foo" alias always triggers getFoo regardless of getMethods order
        for (int i = 0; i < interfaces.length; i++) {
            superTypeNames[i] = p(interfaces[i]);
            for ( Method method : interfaces[i].getMethods() ) {
                final String name = method.getName();
                if ( Modifier.isStatic(method.getModifiers()) ) continue;
                if ( implClass != null ) { // only override default methods if present in implementing class
                    if ( ! Modifier.isAbstract(method.getModifiers()) && ! implClass.getMethods().containsKey(name) ) {
                        continue;
                    }
                }
                List<Method> methods = simpleToAll.get(name);
                if (methods == null) {
                    simpleToAll.put(name, methods = new ArrayList<Method>(6));

                    if ( name.startsWith("is") && name.length() > 2 ) {
                        final String getName = "get" + name.substring(2);
                        List<Method> getMethods = simpleToAll.get(getName);
                        if ( getMethods != null ) { // remove and re-add so that getFoo is after isFoo
                            simpleToAll.remove(getName);
                            simpleToAll.put(getName, getMethods);
                        }
                    }
                }
                methods.add(method);
            }
        }
        return simpleToAll;
    }

    // NOTE: assuming this is only used for interface-impl generation from: Java.newInterfaceImpl
    public static Class createOldStyleImplClass(Class[] superTypes, RubyClass rubyClass, Ruby ruby, String name, ClassDefiningClassLoader classLoader) {
        String[] superTypeNames = new String[superTypes.length];

        // interfaces now do have a convention that they only override an interface default method
        // if a Ruby method (stub) is present in the implementing Ruby class :
        Map<String, List<Method>> simpleToAll = buildSimpleToAllMap(superTypes, superTypeNames, rubyClass);

        Class newClass = defineOldStyleImplClass(ruby, name, superTypeNames, simpleToAll, classLoader);

        return newClass;
    }

    // NOTE: only used for interface class generation from ... Java.generateRealClass
    public static Class createRealImplClass(Class superClass, Class<?>[] interfaces, RubyClass rubyClass, Ruby ruby, String name) {
        String[] superTypeNames = new String[interfaces.length];

        // interfaces now do have a convention that they only override an interface default method
        // if a Ruby method (stub) is present in the implementing Ruby class :
        Map<String, List<Method>> simpleToAll = buildSimpleToAllMap(interfaces, superTypeNames, rubyClass);

        Class newClass = defineRealImplClass(ruby, name, superClass, superTypeNames, simpleToAll);

        // Confirm all interfaces got implemented
        for (Class<?> ifc : interfaces) {
            assert ifc.isAssignableFrom(newClass);
        }

        return newClass;
    }

    /**
     * This variation on defineImplClass uses all the classic type coercion logic
     * for passing args and returning results.
     *
     * @param ruby
     * @param name
     * @param superTypeNames
     * @param simpleToAll
     * @return
     */
    public static Class defineOldStyleImplClass(final Ruby ruby, final String name,
        final String[] superTypeNames, final Map<String, List<Method>> simpleToAll,
        final ClassDefiningClassLoader loader) {

        Class newClass;
        synchronized (loader) {
            // try to load the specified name; only if that fails, try to define the class
            try {
                newClass = loader.loadClass(name);
            }
            catch (ClassNotFoundException ex) {
                ClassWriter cw = ASM.newClassWriter(loader.asClassLoader());
                String pathName = name.replace('.', '/');

                // construct the class, implementing all supertypes
                cw.visit(V_BC, ACC_PUBLIC | ACC_SUPER, pathName, null, p(Object.class), superTypeNames);
                cw.visitSource(pathName + ".gen", null);

                // fields needed for dispatch and such
                cw.visitField(ACC_STATIC | ACC_FINAL | ACC_PRIVATE, "$runtimeCache", ci(RuntimeCache.class), null, null).visitEnd();
                cw.visitField(ACC_PRIVATE | ACC_FINAL, "$self", ci(IRubyObject.class), null, null).visitEnd();

                // create static init
                SkinnyMethodAdapter clinitMethod = new SkinnyMethodAdapter(cw, ACC_PUBLIC | ACC_STATIC, "<clinit>", sig(void.class), null, null);

                // create constructor
                SkinnyMethodAdapter initMethod = new SkinnyMethodAdapter(cw, ACC_PUBLIC, "<init>", sig(void.class, IRubyObject.class), null, null);
                initMethod.aload(0);
                initMethod.invokespecial(p(Object.class), "<init>", sig(void.class));

                // store the wrapper
                initMethod.aload(0);
                initMethod.aload(1);
                initMethod.putfield(pathName, "$self", ci(IRubyObject.class));

                // end constructor
                initMethod.voidreturn();
                initMethod.end();

                int cacheSize = 0;

                final HashSet<String> implementedNames = new HashSet<String>();

                // for each simple method name, implement the complex methods, calling the simple version
                for (Map.Entry<String, List<Method>> entry : simpleToAll.entrySet()) {
                    final String simpleName = entry.getKey();
                    final List<Method> methods = entry.getValue();
                    Set<String> nameSet = JavaUtil.getRubyNamesForJavaName(simpleName, methods);

                    implementedNames.clear();

                    for (int i = 0; i < methods.size(); i++) {
                        final Method method = methods.get(i);
                        final Class[] paramTypes = method.getParameterTypes();
                        final Class returnType = method.getReturnType();

                        String fullName = simpleName + prettyParams(paramTypes);
                        if (implementedNames.contains(fullName)) continue;
                        implementedNames.add(fullName);

                        // indices for temp values
                        final int baseIndex = calcBaseIndex(paramTypes, 1);

                        SkinnyMethodAdapter mv = new SkinnyMethodAdapter(
                                cw, ACC_PUBLIC, simpleName, sig(returnType, paramTypes), null, null);
                        mv.start();
                        mv.line(1);

                        switch ( simpleName ) {
                            // TODO: this code should really check if a Ruby equals method is implemented or not.
                            case "equals" :
                                if ( defineDefaultEquals(2, mv, paramTypes, returnType) ) ;
                                else defineOldStyleBody(mv, pathName, simpleName, paramTypes, returnType, baseIndex, cacheSize++, nameSet);
                                break;
                            case "hashCode" :
                                if ( defineDefaultHashCode(3, mv, paramTypes, returnType) ) ;
                                else defineOldStyleBody(mv, pathName, simpleName, paramTypes, returnType, baseIndex, cacheSize++, nameSet);
                                break;
                            case "toString" :
                                if ( defineDefaultToString(4, mv, paramTypes, returnType) ) ;
                                else defineOldStyleBody(mv, pathName, simpleName, paramTypes, returnType, baseIndex, cacheSize++, nameSet);
                                break;
                            case "__ruby_object" :
                                if ( paramTypes.length == 0 && returnType == IRubyObject.class ) {
                                    mv.aload(0);
                                    mv.getfield(pathName, "$self", ci(IRubyObject.class));
                                    mv.areturn();
                                    break;
                                }
                            default : // cacheIndex = cacheSize++;
                                defineOldStyleBody(mv, pathName, simpleName, paramTypes, returnType, baseIndex, cacheSize++, nameSet);
                                break;
                        }

                        mv.end();
                    }
                }

                // end setup method
                clinitMethod.newobj(p(RuntimeCache.class));
                clinitMethod.dup();
                clinitMethod.invokespecial(p(RuntimeCache.class), "<init>", sig(void.class));
                clinitMethod.dup();
                clinitMethod.ldc(cacheSize);
                clinitMethod.invokevirtual(p(RuntimeCache.class), "initMethodCache", sig(void.class, int.class));
                clinitMethod.putstatic(pathName, "$runtimeCache", ci(RuntimeCache.class));
                clinitMethod.voidreturn();
                clinitMethod.end();

                // end class
                cw.visitEnd();

                // create the class
                final byte[] bytecode = cw.toByteArray();
                newClass = loader.defineClass(name, bytecode);
                if ( DEBUG ) writeClassFile(name, bytecode);
            }
        }

        return newClass;
    }

    private static void defineOldStyleBody(SkinnyMethodAdapter mv, final String pathName,
        final String simpleName, final Class[] paramTypes, final Class returnType,
        final int baseIndex, final int cacheIndex, final Set<String> nameSet) {

        final int selfIndex = baseIndex;
        final int rubyIndex = selfIndex + 1;

        mv.line(5);

        // prepare temp locals
        mv.aload(0);
        mv.getfield(pathName, "$self", ci(IRubyObject.class));
        mv.astore(selfIndex);
        mv.aload(selfIndex);
        mv.invokeinterface(p(IRubyObject.class), "getRuntime", sig(Ruby.class));
        mv.astore(rubyIndex);

        // get method from cache
        mv.getstatic(pathName, "$runtimeCache", ci(RuntimeCache.class));
        mv.aload(selfIndex);
        mv.ldc(cacheIndex);
        for (String eachName : nameSet) {
            mv.ldc(eachName);
        }
        mv.invokevirtual(p(RuntimeCache.class), "searchWithCache",
            sig(DynamicMethod.class, params(IRubyObject.class, int.class, String.class, nameSet.size())));

        // get current context
        mv.aload(rubyIndex);
        mv.invokevirtual(p(Ruby.class), "getCurrentContext", sig(ThreadContext.class));

        // load self, class, and name
        mv.aloadMany(selfIndex, selfIndex);
        mv.invokeinterface(p(IRubyObject.class), "getMetaClass", sig(RubyClass.class));
        mv.ldc(simpleName);

        // coerce arguments
        coerceArgumentsToRuby(mv, paramTypes, rubyIndex);

        // load null block
        mv.getstatic(p(Block.class), "NULL_BLOCK", ci(Block.class));

        // invoke method
        mv.line(13);
        mv.invokevirtual(p(DynamicMethod.class), "call", sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, RubyModule.class, String.class, IRubyObject[].class, Block.class));

        coerceResultAndReturn(mv, returnType);
    }

    /**
     * This variation on defineImplClass uses all the classic type coercion logic
     * for passing args and returning results.
     *
     * @param runtime
     * @param name
     * @param superTypeNames
     * @param simpleToAll
     * @return
     */
    public static Class defineRealImplClass(final Ruby runtime, final String name,
        final Class superClass, final String[] superTypeNames,
        final Map<String, List<Method>> simpleToAll) {

        final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
        final String pathName = name.replace('.', '/');

        boolean isRubyHierarchy = RubyBasicObject.class.isAssignableFrom(superClass);

        // construct the class, implementing all supertypes
        if (isRubyHierarchy) {
            // Ruby hierarchy...just extend it
            cw.visit(V_BC, ACC_PUBLIC | ACC_SUPER, pathName, null, p(superClass), superTypeNames);
        }
        else {
            // Non-Ruby hierarchy; add IRubyObject
            String[] plusIRubyObject = new String[superTypeNames.length + 1];
            plusIRubyObject[0] = p(IRubyObject.class);
            System.arraycopy(superTypeNames, 0, plusIRubyObject, 1, superTypeNames.length);

            cw.visit(V_BC, ACC_PUBLIC | ACC_SUPER, pathName, null, p(superClass), plusIRubyObject);
        }
        cw.visitSource(pathName + ".gen", null);

        // fields needed for dispatch and such
        cw.visitField(ACC_STATIC | ACC_FINAL | ACC_PRIVATE, "$runtimeCache", ci(RuntimeCache.class), null, null).visitEnd();

        // create static init
        SkinnyMethodAdapter clinitMethod = new SkinnyMethodAdapter(cw, ACC_PUBLIC | ACC_STATIC, "<clinit>", sig(void.class), null, null);

        // create constructor
        SkinnyMethodAdapter initMethod = new SkinnyMethodAdapter(cw, ACC_PUBLIC, "<init>", sig(void.class, Ruby.class, RubyClass.class), null, null);

        if (isRubyHierarchy) {
            // superclass is in the Ruby object hierarchy; invoke typical Ruby superclass constructor
            initMethod.aloadMany(0, 1, 2);
            initMethod.invokespecial(p(superClass), "<init>", sig(void.class, Ruby.class, RubyClass.class));
        }
        else {
            // superclass is not in Ruby hierarchy; store objects and call no-arg super constructor
            cw.visitField(ACC_FINAL | ACC_PRIVATE, "$ruby", ci(Ruby.class), null, null).visitEnd();
            cw.visitField(ACC_FINAL | ACC_PRIVATE, "$rubyClass", ci(RubyClass.class), null, null).visitEnd();

            initMethod.aloadMany(0, 1);
            initMethod.putfield(pathName, "$ruby", ci(Ruby.class));
            initMethod.aloadMany(0, 2);
            initMethod.putfield(pathName, "$rubyClass", ci(RubyClass.class));

            // only no-arg super constructor supported right now
            initMethod.aload(0);
            initMethod.invokespecial(p(superClass), "<init>", sig(void.class));
        }
        initMethod.voidreturn();
        initMethod.end();

        if (isRubyHierarchy) { // override toJava
            SkinnyMethodAdapter toJavaMethod = new SkinnyMethodAdapter(cw, ACC_PUBLIC, "toJava", sig(Object.class, Class.class), null, null);
            toJavaMethod.aload(0);
            toJavaMethod.areturn();
            toJavaMethod.end();
        }
        else { // decorate with stubbed IRubyObject methods
            BasicObjectStubGenerator.addBasicObjectStubsToClass(cw);

            // add getRuntime and getMetaClass impls based on captured fields
            SkinnyMethodAdapter getRuntimeMethod = new SkinnyMethodAdapter(cw, ACC_PUBLIC, "getRuntime", sig(Ruby.class), null, null);
            getRuntimeMethod.aload(0);
            getRuntimeMethod.getfield(pathName, "$ruby", ci(Ruby.class));
            getRuntimeMethod.areturn();
            getRuntimeMethod.end();

            SkinnyMethodAdapter getMetaClassMethod = new SkinnyMethodAdapter(cw, ACC_PUBLIC, "getMetaClass", sig(RubyClass.class), null, null);
            getMetaClassMethod.aload(0);
            getMetaClassMethod.getfield(pathName, "$rubyClass", ci(RubyClass.class));
            getMetaClassMethod.areturn();
            getMetaClassMethod.end();
        }

        int cacheSize = 0;

        final HashSet<String> implementedNames = new HashSet<String>();

        // for each simple method name, implement the complex methods, calling the simple version
        for (Map.Entry<String, List<Method>> entry : simpleToAll.entrySet()) {
            final String simpleName = entry.getKey();
            final List<Method> methods = entry.getValue();
            Set<String> nameSet = JavaUtil.getRubyNamesForJavaName(simpleName, methods);

            implementedNames.clear();

            for (int i = 0; i < methods.size(); i++) {
                final Method method = methods.get(i);
                final Class[] paramTypes = method.getParameterTypes();
                final Class returnType = method.getReturnType();

                String fullName = simpleName + prettyParams(paramTypes);
                if (implementedNames.contains(fullName)) continue;
                implementedNames.add(fullName);

                // indices for temp values
                final int baseIndex = calcBaseIndex(paramTypes, 1);

                SkinnyMethodAdapter mv = new SkinnyMethodAdapter(
                        cw, ACC_PUBLIC, simpleName, sig(returnType, paramTypes), null, null);
                mv.start();
                mv.line(1);

                switch ( simpleName ) { // cacheIndex = cacheSize++;

                    case "equals" :
                        if ( paramTypes.length == 1 && paramTypes[0] == Object.class && returnType == Boolean.TYPE ) {
                            defineRealEqualsWithFallback(mv, pathName, simpleName, paramTypes, returnType, baseIndex, cacheSize++, nameSet);
                        }
                        else defineRealBody(mv, pathName, simpleName, paramTypes, returnType, baseIndex, cacheSize++, nameSet);
                        break;
                    case "hashCode" :
                        if ( paramTypes.length == 0 && returnType == Integer.TYPE ) {
                            defineRealHashCodeWithFallback(mv, pathName, simpleName, paramTypes, returnType, baseIndex, cacheSize++, nameSet);
                        }
                        else defineRealBody(mv, pathName, simpleName, paramTypes, returnType, baseIndex, cacheSize++, nameSet);
                        break;
                    case "toString" :
                        if ( paramTypes.length == 0 && returnType == String.class ) {
                            defineRealToStringWithFallback(mv, pathName, simpleName, paramTypes, returnType, baseIndex, cacheSize++, nameSet);
                        }
                        else defineRealBody(mv, pathName, simpleName, paramTypes, returnType, baseIndex, cacheSize++, nameSet);
                        break;
                    default :
                        defineRealBody(mv, pathName, simpleName, paramTypes, returnType, baseIndex, cacheSize++, nameSet);
                        break;
                }

                mv.end();
            }
        }

        // end setup method
        clinitMethod.newobj(p(RuntimeCache.class));
        clinitMethod.dup();
        clinitMethod.invokespecial(p(RuntimeCache.class), "<init>", sig(void.class));
        clinitMethod.dup();
        clinitMethod.ldc(cacheSize);
        clinitMethod.invokevirtual(p(RuntimeCache.class), "initMethodCache", sig(void.class, int.class));
        clinitMethod.putstatic(pathName, "$runtimeCache", ci(RuntimeCache.class));
        clinitMethod.voidreturn();
        clinitMethod.end();

        // end class
        cw.visitEnd();

        // first try to find the class
        Class newClass = null;
        for(Loader loader : runtime.getInstanceConfig().getExtraLoaders()) {
            try {
                newClass = loader.loadClass(name);
                break;
            }
            catch(ClassNotFoundException ignored) {
            }
        }

        final ClassDefiningJRubyClassLoader loader;
        if (superClass.getClassLoader() instanceof ClassDefiningJRubyClassLoader) {
            loader = new ClassDefiningJRubyClassLoader(superClass.getClassLoader());
        } else {
            loader = new ClassDefiningJRubyClassLoader(runtime.getJRubyClassLoader());
        }

        if (newClass == null) {
            try {
                newClass = loader.loadClass(name);
            }
            catch (ClassNotFoundException ignored) {
            }
        }

        // create the class
        if (newClass == null) {
            final byte[] bytecode = cw.toByteArray();
            MultiClassLoader multiClassLoader = new MultiClassLoader(superClass.getClassLoader());
            for(Loader cLoader : runtime.getInstanceConfig().getExtraLoaders()) {
                multiClassLoader.addClassLoader(cLoader.getClassLoader());
            }
            try {
                newClass = new ClassDefiningJRubyClassLoader(multiClassLoader).defineClass(name, bytecode);
            }
            catch(Error ignored) {
            }
            if (newClass == null) {
                newClass = loader.defineClass(name, bytecode);
            }
            if ( DEBUG ) writeClassFile(name, bytecode);
        }

        return newClass;
    }

    private static void defineRealBody(SkinnyMethodAdapter mv, final String pathName,
        final String simpleName, final Class[] paramTypes, final Class returnType,
        final int baseIndex, final int cacheIndex, final Set<String> nameSet) {

        final int rubyIndex = baseIndex + 1;

        mv.line(5);

        // prepare temp locals
        mv.aload(0);
        mv.invokeinterface(p(IRubyObject.class), "getRuntime", sig(Ruby.class));
        mv.astore(rubyIndex);

        // get method from cache
        mv.getstatic(pathName, "$runtimeCache", ci(RuntimeCache.class));
        mv.aload(0);
        mv.ldc(cacheIndex);
        for (String eachName : nameSet) {
            mv.ldc(eachName);
        }
        mv.invokevirtual(p(RuntimeCache.class), "searchWithCache",
                sig(DynamicMethod.class, params(IRubyObject.class, int.class, String.class, nameSet.size())));

        // get current context
        mv.aload(rubyIndex);
        mv.invokevirtual(p(Ruby.class), "getCurrentContext", sig(ThreadContext.class));

        // load self, class, and name
        mv.aloadMany(0, 0);
        mv.invokeinterface(p(IRubyObject.class), "getMetaClass", sig(RubyClass.class));
        mv.ldc(simpleName);

        // coerce arguments
        coerceArgumentsToRuby(mv, paramTypes, rubyIndex);

        // load null block
        mv.getstatic(p(Block.class), "NULL_BLOCK", ci(Block.class));

        // invoke method
        mv.line(13);
        mv.invokevirtual(p(DynamicMethod.class), "call", sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, RubyModule.class, String.class, IRubyObject[].class, Block.class));

        coerceResultAndReturn(mv, returnType);
    }

    private static void defineRealBodyWithFallback(SkinnyMethodAdapter mv, final String pathName,
        final String simpleName, final Class[] paramTypes, final Class returnType,
        final int baseIndex, final int cacheIndex, final Set<String> nameSet) {

        final int rubyIndex = baseIndex + 1;

        //mv.line(5);

        // prepare temp locals
        mv.aload(0);
        mv.invokeinterface(p(IRubyObject.class), "getRuntime", sig(Ruby.class));
        mv.astore(rubyIndex);

        // get method from cache
        mv.getstatic(pathName, "$runtimeCache", ci(RuntimeCache.class));
        mv.aload(0);
        mv.ldc(cacheIndex);
        for (String eachName : nameSet) {
            mv.ldc(eachName);
        }
        mv.invokevirtual(p(RuntimeCache.class), "searchWithCacheNoMethodMissing",
                sig(DynamicMethod.class, params(IRubyObject.class, int.class, String.class, nameSet.size())));
        final int methodIndex = baseIndex + 2;
        mv.astore(methodIndex);

        Label fallback = new Label();
        mv.aload(methodIndex);
        mv.ifnull(fallback);

        mv.aload(methodIndex); // method (!= null)

        // get current context
        mv.aload(rubyIndex);
        mv.invokevirtual(p(Ruby.class), "getCurrentContext", sig(ThreadContext.class));

        // load self, class, and name
        mv.aloadMany(0, 0);
        mv.invokeinterface(p(IRubyObject.class), "getMetaClass", sig(RubyClass.class));
        mv.ldc(simpleName);

        // coerce arguments
        coerceArgumentsToRuby(mv, paramTypes, rubyIndex);

        // load null block
        mv.getstatic(p(Block.class), "NULL_BLOCK", ci(Block.class));

        // invoke method
        //mv.line(13);
        mv.invokevirtual(p(DynamicMethod.class), "call", sig(IRubyObject.class, ThreadContext.class, IRubyObject.class, RubyModule.class, String.class, IRubyObject[].class, Block.class));

        coerceResultAndReturn(mv, returnType);

        // fallback (default) impl :
        mv.label(fallback);
        switch ( simpleName ) {
            case "equals" : objectEquals(-1, mv); break;
            case "hashCode" : objectHashCode(-1, mv); break;
            case "toString" : objectToString(-1, mv); break;
            default : throw new UnsupportedOperationException(simpleName);
        }
    }

    private static void defineRealEqualsWithFallback(SkinnyMethodAdapter mv, final String pathName,
        final String simpleName, final Class[] paramTypes, final Class returnType,
        final int baseIndex, final int cacheIndex, final Set<String> nameSet) {
        defineRealBodyWithFallback(mv, pathName, simpleName, paramTypes, returnType, baseIndex, cacheIndex, nameSet);
    }

    private static void defineRealHashCodeWithFallback(SkinnyMethodAdapter mv, final String pathName,
        final String simpleName, final Class[] paramTypes, final Class returnType,
        final int baseIndex, final int cacheIndex, final Set<String> nameSet) {
        defineRealBodyWithFallback(mv, pathName, simpleName, paramTypes, returnType, baseIndex, cacheIndex, nameSet);
    }

    private static void defineRealToStringWithFallback(SkinnyMethodAdapter mv, final String pathName,
        final String simpleName, final Class[] paramTypes, final Class returnType,
        final int baseIndex, final int cacheIndex, final Set<String> nameSet) {
        defineRealBodyWithFallback(mv, pathName, simpleName, paramTypes, returnType, baseIndex, cacheIndex, nameSet);
    }

    private static boolean defineDefaultEquals(final int line, SkinnyMethodAdapter mv,
        final Class[] paramTypes, final Class returnType) {

        if ( paramTypes.length == 1 && paramTypes[0] == Object.class && returnType == Boolean.TYPE ) {
            objectEquals(line, mv);
            return true;
        }
        return false;
    }

    private static void objectEquals(final int line, SkinnyMethodAdapter mv) {
        if ( line > 0 ) mv.line(line);
        mv.aload(0);
        mv.aload(1);
        mv.invokespecial(p(Object.class), "equals", sig(Boolean.TYPE, params(Object.class)));
        mv.ireturn();
    }

    private static boolean defineDefaultHashCode(final int line, SkinnyMethodAdapter mv,
        final Class[] paramTypes, final Class returnType) {

        if ( paramTypes.length == 0 && returnType == Integer.TYPE ) {
            objectHashCode(line, mv);
            return true;
        }
        return false;
    }

    private static void objectHashCode(final int line, SkinnyMethodAdapter mv) {
        if ( line > 0 ) mv.line(line);
        mv.aload(0);
        mv.invokespecial(p(Object.class), "hashCode", sig(Integer.TYPE));
        mv.ireturn();
    }

    private static boolean defineDefaultToString(final int line, SkinnyMethodAdapter mv,
        final Class[] paramTypes, final Class returnType) {

        if ( paramTypes.length == 0 && returnType == String.class ) {
            objectToString(line, mv);
            return true;
        }
        return false;
    }

    private static void objectToString(final int line, SkinnyMethodAdapter mv) {
        if ( line > 0 ) mv.line(line);
        mv.aload(0);
        mv.invokespecial(p(Object.class), "toString", sig(String.class));
        mv.areturn();
    }

    private static void writeClassFile(final String name, final byte[] bytecode) {
        try (FileOutputStream fos = new FileOutputStream(name + ".class")) {
            fos.write(bytecode);
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public static void coerceArgumentsToRuby(SkinnyMethodAdapter mv, Class[] paramTypes, int rubyIndex) {
        // load arguments into IRubyObject[] for dispatch
        if (paramTypes.length != 0) {
            mv.pushInt(paramTypes.length);
            mv.anewarray(p(IRubyObject.class));

            // TODO: make this do specific-arity calling
            for (int i = 0, argIndex = 1; i < paramTypes.length; i++) {
                Class paramType = paramTypes[i];
                mv.dup();
                mv.pushInt(i);
                // convert to IRubyObject
                if (paramTypes[i].isPrimitive()) {
                    mv.aload(rubyIndex);
                    if (paramType == byte.class || paramType == short.class || paramType == char.class || paramType == int.class) {
                        mv.iload(argIndex++);
                        mv.invokestatic(p(JavaUtil.class), "convertJavaToRuby", sig(IRubyObject.class, Ruby.class, int.class));
                    } else if (paramType == long.class) {
                        mv.lload(argIndex);
                        argIndex += 2; // up two slots, for long's two halves
                        mv.invokestatic(p(JavaUtil.class), "convertJavaToRuby", sig(IRubyObject.class, Ruby.class, long.class));
                    } else if (paramType == float.class) {
                        mv.fload(argIndex++);
                        mv.invokestatic(p(JavaUtil.class), "convertJavaToRuby", sig(IRubyObject.class, Ruby.class, float.class));
                    } else if (paramType == double.class) {
                        mv.dload(argIndex);
                        argIndex += 2; // up two slots, for long's two halves
                        mv.invokestatic(p(JavaUtil.class), "convertJavaToRuby", sig(IRubyObject.class, Ruby.class, double.class));
                    } else if (paramType == boolean.class) {
                        mv.iload(argIndex++);
                        mv.invokestatic(p(JavaUtil.class), "convertJavaToRuby", sig(IRubyObject.class, Ruby.class, boolean.class));
                    }
                } else if (!IRubyObject.class.isAssignableFrom(paramType)) {
                    mv.aload(rubyIndex);
                    mv.aload(argIndex++);
                    mv.invokestatic(p(JavaUtil.class), "convertJavaToUsableRubyObject", sig(IRubyObject.class, Ruby.class, Object.class));
                } else {
                    mv.aload(argIndex++);
                }
                mv.aastore();
            }
        } else {
            mv.getstatic(p(IRubyObject.class), "NULL_ARRAY", ci(IRubyObject[].class));
        }
    }

    public static void coerceResultAndReturn(SkinnyMethodAdapter mv, Class returnType) {
        coerceResult(mv, returnType, true);
    }
    
    public static void coerceResult(SkinnyMethodAdapter mv, Class returnType, boolean doReturn) {
        // if we expect a return value, unwrap it
        if (returnType != void.class) {
            // TODO: move the bulk of this logic to utility methods
            if (returnType.isPrimitive()) {
                if (returnType == boolean.class) {
                    mv.getstatic(p(Boolean.class), "TYPE", ci(Class.class));
                    mv.invokeinterface(p(IRubyObject.class), "toJava", sig(Object.class, Class.class));
                    mv.checkcast(p(Boolean.class));
                    mv.invokevirtual(p(Boolean.class), "booleanValue", sig(boolean.class));
                    if (doReturn) mv.ireturn();
                } else {
                    mv.getstatic(p(getBoxType(returnType)), "TYPE", ci(Class.class));
                    mv.invokeinterface(p(IRubyObject.class), "toJava", sig(Object.class, Class.class));
                    if (returnType == byte.class) {
                        mv.checkcast(p(Number.class));
                        mv.invokevirtual(p(Number.class), "byteValue", sig(byte.class));
                        if (doReturn) mv.ireturn();
                    } else if (returnType == short.class) {
                        mv.checkcast(p(Number.class));
                        mv.invokevirtual(p(Number.class), "shortValue", sig(short.class));
                        if (doReturn) mv.ireturn();
                    } else if (returnType == char.class) {
                        mv.checkcast(p(Character.class));
                        mv.invokevirtual(p(Character.class), "charValue", sig(char.class));
                        if (doReturn) mv.ireturn();
                    } else if (returnType == int.class) {
                        mv.checkcast(p(Number.class));
                        mv.invokevirtual(p(Number.class), "intValue", sig(int.class));
                        if (doReturn) mv.ireturn();
                    } else if (returnType == long.class) {
                        mv.checkcast(p(Number.class));
                        mv.invokevirtual(p(Number.class), "longValue", sig(long.class));
                        if (doReturn) mv.lreturn();
                    } else if (returnType == float.class) {
                        mv.checkcast(p(Number.class));
                        mv.invokevirtual(p(Number.class), "floatValue", sig(float.class));
                        if (doReturn) mv.freturn();
                    } else if (returnType == double.class) {
                        mv.checkcast(p(Number.class));
                        mv.invokevirtual(p(Number.class), "doubleValue", sig(double.class));
                        if (doReturn) mv.dreturn();
                    }
                }
            } else {
                // if the return type is not an IRubyObject implementer, coerce to that type before casting
                if (!IRubyObject.class.isAssignableFrom(returnType)) {
                    mv.ldc(Type.getType(returnType));
                    mv.invokeinterface(
                        p(IRubyObject.class), "toJava", sig(Object.class, Class.class));
                }
                mv.checkcast(p(returnType));
                if (doReturn) mv.areturn();
            }
        } else if (doReturn) {
            mv.voidreturn();
        }
    }

    public static int calcBaseIndex(final Class[] params, int baseIndex) {
        for (Class paramType : params) {
            if (paramType == double.class || paramType == long.class) {
                baseIndex += 2;
            } else {
                baseIndex += 1;
            }
        }
        return baseIndex;
    }

    private static final String CONCRETE_CTOR_SIG = sig(void.class, ConcreteJavaProxy.class, boolean.class,
            IRubyObject[].class, Block.class, Ruby.class, RubyClass.class);

    /**
     * Main switch constructor. Required for concrete reification
     */
    public static void makeConcreteConstructorSwitch(ClassWriter cw, PositionAware initPosition, int superpos,
            boolean hasParent, ConcreteJavaReifier cjr, JavaConstructor[] constructors) {
        // TODO: add source position of super call?

        /*
         * This generates the code template in lines of //// show what code is being generated
         * TODO: link and put on wiki?
         * Generated method:
   // $FF: synthetic method
   protected MyClass(ConcreteJavaProxy var1, boolean var2, IRubyObject[] var3, Block var4, Ruby var5, RubyClass var6) {
      this.this$rubyObject = var1;
      SplitCtorData var10000 = var1.splitInitialized(var2 ? rubyClass : var6, var3, var4, this$rubyCtorCache);
      Object[] var7 = var10000.arguments;
      switch(var10000.ctorIndex) {
      case 0:
         super((String)var7[0], (Boolean)var7[1]);
         break;
      case 1:
         super(((Number)var7[0]).intValue(), (String)var7[1]);
         break;
      default:
         throw var5.newNoMethodError("No available java superconstructors match that type signature", "super.<init>", var10000.rbarguments);
      }

      var1.setObject(this);
      var1.finishInitialize(var10000);
   }
         */
        // (rubyobject, isSuperCall, args, block, ruby, class)
        SkinnyMethodAdapter m = new SkinnyMethodAdapter(cw, ACC_PROTECTED | ACC_SYNTHETIC, "<init>", CONCRETE_CTOR_SIG,
                null, null);

        // set args for init
        final int thisIndex = 0;
        final int cjpIndex = 1;
        final int isSuperCallIndex = 2;
        final int rubyArrayIndex = 3;
        final int blockIndex = 4;
        final int rubyIndex = 5;
        final int rubyClassIndex = 6;

        m.line(initPosition.getLine());

        m.aload(cjpIndex); // cjp is at arg 1 (to support alloc+initialize seperation)

        m.dup(); // rubyobject
        m.aload(thisIndex); // uninitialized this
        m.swap();
        m.putfield(cjr.javaPath, ConcreteJavaReifier.RUBY_OBJECT_FIELD, cjr.rubyName);

        //// SplitCtorData c = this$rubyObject.splitInitialized(this.$rubyInitArgs);
        m.iload(isSuperCallIndex);

        Label normal = new Label();
        Label done = new Label();
        m.iffalse(normal);//// if (super branch) {
        m.getstatic(cjr.javaPath, cjr.RUBY_CLASS_FIELD, ci(RubyClass.class)); // use static if this is from the super
        m.go_to(done);
        //// else { // normal branch
        m.label(normal);
        m.aload(rubyClassIndex); // rubyclass
        m.label(done);

        m.aload(rubyArrayIndex);
        m.aload(blockIndex); // load block from arg 3
        if (!hasParent) {
            m.getstatic(cjr.javaPath, cjr.RUBY_CTOR_CACHE_FIELD, ci(JCtorCache.class));
        } else {
            m.aconst_null();
        }
        m.invokevirtual(cjr.rubyPath, "splitInitialized",
                sig(SplitCtorData.class, RubyClass.class, IRubyObject[].class, Block.class, JCtorCache.class)); // pushes splitctordata

        m.dup(); // splitctordata (results of splitInitialized)

        m.line(superpos); // mark this line as the super call, so the stack trace is slightly accurate.

        // top of stack is now the arg list ruby array

        if (!hasParent) {

            //// switch(c.ctorIndex)
            m.dup();
            m.getfield(p(SplitCtorData.class), "ctorIndex", ci(int.class));
            // ..., scd, index
            m.swap();
            //// ra = c.arguments;
            m.getfield(p(SplitCtorData.class), "arguments", ci(Object[].class));
            m.astore(rubyArrayIndex); // ....
            Label defaultLabel = new Label();
            Label[] cases = new Label[constructors.length]; // note: offset by one from index
            for (int i = 0; i < constructors.length; i++) {
                cases[i] = new Label();
            }
            Label endofswitch = new Label();
            //// switch (...)
            m.tableswitch(0, constructors.length - 1, defaultLabel, cases);
            {
                // default: throw runtime.newNoMethodError("...", "super.<init>", [])
                m.label(defaultLabel);
                m.aload(rubyIndex);
                m.swap();
                m.ldc("No available java superconstructors match that type signature");
                m.swap();
                m.ldc("super.<init>");
                m.swap();
                m.getfield(p(SplitCtorData.class), "rbarguments", ci(IRubyObject[].class));
                m.invokevirtual(p(Ruby.class), "newNoMethodError",
                        sig(RaiseException.class, String.class, String.class, IRubyObject[].class));
                m.athrow();

                // case n:
                for (int i = 0; i < constructors.length; i++) {
                    m.label(cases[i]);

                    // setup super call
                    m.aload(thisIndex); // ..., uninitialized this

                    Class[] destType = constructors[i].getParameterTypes();

                    // coerce args. No error checking as the forTypes() call should have done that for us
                    //// super((long)ra[0]);
                    for (int argi = 0; argi < destType.length; argi++) {
                        m.aload(rubyArrayIndex);
                        m.pushInt(argi);
                        m.aaload();
                        if (destType[argi].isPrimitive()) {
                            makeGenerator(m).unbox(Type.getType(destType[argi]));
                        } else {
                            m.checkcast(p(destType[argi]));
                        }
                    }
                    //// super(*args)
                    m.invokespecial(p(cjr.reifiedParent), "<init>", sig(void.class, destType));
                    m.go_to(endofswitch);
                }
            }

            m.label(endofswitch);
        } else {
            // set up the stack for the super call. Note, we need to bubble up the 4th arg (on top of the stack now)
            m.aload(thisIndex); // uninitialized this
            m.swap();
            m.aload(cjpIndex); // cjp
            m.swap();
            m.iconst_1(); // true, we are super
            m.swap();

            m.dup();
            m.getfield(p(SplitCtorData.class), "rbarguments", ci(IRubyObject[].class));
            m.swap();


            m.getfield(p(SplitCtorData.class), "block", ci(Block.class));
            m.aload(rubyIndex); // ruby
            m.aload(rubyClassIndex); // rubyclass
            m.invokespecial(p(cjr.reifiedParent), "<init>", CONCRETE_CTOR_SIG);
        }

        // This is the start of the method, but lets move it away from the super call to be slightly nicer to stack
        // traces
        m.line(initPosition.getLine());

        // implied: if (this.$rubyObject.getObject() == null) // only checked on non-ctor paths
        //// (this.$rubyObject.setObject(this))

        m.aload(thisIndex); // initialized this
        m.aload(cjpIndex);// ..., this, rubyobj
        m.dup_x1(); // rubyobject, this, rubyobject
        m.swap(); // ..., rubyobject, rubyobject, this
        m.invokevirtual(p(ConcreteJavaProxy.class), "setObject", sig(void.class, Object.class));

        //// continuation.callMethod(ruby.getTheadContext(), "call")
        m.swap();
        m.invokevirtual(p(ConcreteJavaProxy.class), "finishInitialize", sig(void.class, SplitCtorData.class));

        m.voidreturn();
        m.end();
    }

    /**
     * Public access for IRubyObject constructor
     */
    public static void makeConcreteConstructorIROProxy(ClassWriter cw, PositionAware initPosition,
            ConcreteJavaReifier cjr) {

        // (rubyobject, isSuperCall, args, block, ruby, class)
        String sig = sig(void.class, ConcreteJavaProxy.class, IRubyObject[].class, Block.class, Ruby.class,
                RubyClass.class);
        SkinnyMethodAdapter m = new SkinnyMethodAdapter(cw, ACC_PUBLIC | ACC_SYNTHETIC, "<init>", sig, null, null);

        m.line(initPosition.getLine());

        m.aload(0);
        m.aload(1);
        m.iconst_0(); // false, not called from subclass
        m.aload(2);
        m.aload(3);
        m.aload(4);
        m.aload(5);
        m.invokespecial(cjr.javaPath, "<init>", CONCRETE_CTOR_SIG);

        m.voidreturn();
        m.end();
    }

    /**
     * Defines a constructor that delegates to the main switch constructor
     * 
     * @param cw           class builder
     * @param initPosition source code position of initialize
     * @param hasRuby      If this method accepts ruby &amp; rubyclass on the end (auto-added)
     * @param ctorTypes    signature, minus any ruby arguments
     * @param nested       If this is a nested/secondary ruby class ([ruby &lt;]+ ruby &lt; java )
     */
    public static String makeConcreteConstructorProxy(ClassWriter cw, PositionAware initPosition, boolean hasRuby,
            ConcreteJavaReifier cjr, Class[] ctorTypes, boolean nested) {
        Class clazz;
        if (Map.class.isAssignableFrom(cjr.reifiedParent)) {
            clazz = MapJavaProxy.class;
        } else {
            clazz = ConcreteJavaProxy.class;
        }
        String sig = hasRuby ? sig(void.class, cjr.join(ctorTypes, Ruby.class, RubyClass.class))
                : sig(void.class, ctorTypes);
        SkinnyMethodAdapter m = new SkinnyMethodAdapter(cw, ACC_PUBLIC, "<init>", sig, null, null);
        m.line(initPosition.getLine());

        // set args for init
        final int baseIndex = RealClassGenerator.calcBaseIndex(ctorTypes, 1);
        final int rubyIndex = baseIndex;
        final int rubyClassIndex = baseIndex + 1;

        // save for argument converter
        if (!hasRuby) {
            m.getstatic(cjr.javaPath, cjr.RUBY_FIELD, ci(Ruby.class));
            m.getstatic(cjr.javaPath, cjr.RUBY_CLASS_FIELD, ci(RubyClass.class));
            m.astore(rubyClassIndex); // rubyclass
            m.astore(rubyIndex); // ruby
        }

        m.aload(0); // uninitialized this

        //// new ConcreteJavaProxy(ruby, rubyClass);
        m.newobj(p(clazz));
        m.dup(); // rubyobject
        m.aload(rubyIndex); // ruby
        m.aload(rubyClassIndex); // rubyclass
        m.invokespecial(p(clazz), "<init>", sig(void.class, Ruby.class, RubyClass.class));

        if (nested) m.iconst_1();
        else m.iconst_0(); // called from subclass?

        //// new IRubyObject[]{JavaUtil.convertJavaToRuby(var3, var1), JavaUtil.convertJavaToUsableRubyObject(var3, var2)};
        RealClassGenerator.coerceArgumentsToRuby(m, ctorTypes, rubyIndex);

        m.getstatic(p(Block.class), "NULL_BLOCK", ci(Block.class));
        m.aload(rubyIndex); // ruby
        m.aload(rubyClassIndex); // rubyclass

        m.invokespecial(cjr.javaPath, "<init>", CONCRETE_CTOR_SIG);

        m.voidreturn();
        m.end();

        return sig;
    }

    public static GeneratorAdapter makeGenerator(SkinnyMethodAdapter m) {
        return new GeneratorAdapter(m.getMethodVisitor(), Opcodes.ACC_PUBLIC, "ignored", m.getSignature());
    }
}
